/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package JTCPconnection;

import java.io.*;
import java.net.*;

/**
 * 
 * @author Martin Dijkstra
 */
public class TCPconnection
{
	IMessageReceiver			_messageReceiver;

	private ConnectionStatus	_status	= ConnectionStatus.NOINIT;

	public static enum ConnectionStatus
	{
		NOINIT, IDLE, LISTEN, CONNECTING, CONNECTED, CONNECTION_LOST, DISCONNECTING
	};

	private int					_port;
	private Socket				_socket;

	private PrintWriter			_out;
	private BufferedReader		_in;

	private ConnectionListener	_connectionListen;
	private TCPlistener			_msgListen;
	private TCPsender			_msgSend;

	private InetAddress			_address;

	/**
	 * Construct a new instance of this class.
	 * 
	 * @param receiver
	 *            The class that implements IMessageReceiver. The method IMessageReceiver.receiveMessage will be called if a message has been received.
	 */
	public TCPconnection(IMessageReceiver receiver)
	{
		this(1337, receiver);
	}

	/**
	 * Construct a new instance of this class.
	 * 
	 * @param port
	 *            The port to use for communication.
	 * @param receiver
	 *            The class that implements IMessageReceiver. The method IMessageReceiver.receiveMessage will be called if a message has been received.
	 */
	public TCPconnection(int port, IMessageReceiver receiver)
	{
		_messageReceiver = receiver;
		_port = port;
		_status = ConnectionStatus.IDLE;
	}

	/**
	 * Attempt to listen for incoming connection requests.
	 * 
	 * @throws TCPconnectionException
	 *             When the status is not idle.
	 */
	public void listen() throws TCPconnectionException
	{
		if (_status == ConnectionStatus.IDLE)
		{
			_connectionListen = new ConnectionListener(this);
			_status = ConnectionStatus.LISTEN;
		}
		else
		{
			throw new TCPconnectionException("TCPconnection.listen(): Failed to start listening: the TCPconnection state was not IDLE");
		}
	}

	/**
	 * Stop accepting connection requests, return to idle state.
	 */
	public void stopListen()
	{
		if (_status == ConnectionStatus.LISTEN)
		{
			_connectionListen.stop();
			_status = ConnectionStatus.IDLE;
		}
	}

	/**
	 * Get the port used.
	 * 
	 * @return The port used by the TCP connection.
	 */
	int getPort()
	{
		return _port;
	}

	/**
	 * Getter for the buffer reader.
	 * 
	 * @return The buffer reader used by this class. Buffer reader contains the messages sent through the port.
	 */
	BufferedReader getBufferedReader()
	{
		return _in;
	}

	/**
	 * Disconnect, call this method when you want to terminate the connection.
	 */
	public void disconnect()
	{
		_status = ConnectionStatus.DISCONNECTING;

		try
		{
			send(null);
		}
		catch (Exception e)
		{}

		try
		{
			_connectionListen.stop();
		}
		catch (Exception e)
		{}
		try
		{
			_msgListen.stop();
		}
		catch (Exception e)
		{}
		try
		{
			_msgSend.stop();
		}
		catch (Exception e)
		{}
		try
		{
			_out.close();
		}
		catch (Exception e)
		{}
		try
		{
			_in.close();
		}
		catch (Exception e)
		{}
		try
		{
			_socket.close();
		}
		catch (Exception e)
		{}

		_address = null;
		_in = null;
		_out = null;
		_connectionListen = null;
		_msgListen = null;
		_socket = null;

		_status = ConnectionStatus.IDLE;

		_messageReceiver.notifyDisconnected();
	}

	/**
	 * Called by ConnectionListener when a suitable connection has been found.
	 * 
	 * @param connectionSocket
	 *            The socket used for the connection.
	 */
	void acceptConnection(Socket connectionSocket)
	{
		_socket = connectionSocket;

		try
		{
			_out = new PrintWriter(_socket.getOutputStream(), true);
			_in = new BufferedReader(new InputStreamReader(_socket.getInputStream()));
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		_status = ConnectionStatus.CONNECTED;
		startMessageSending();
		startMessageListening();
		_messageReceiver.notifyConnected();
	}

	/**
	 * Check if TCPconnection is connected or not
	 * 
	 * @return True when connection status is "CONNECTED", False when otherwise
	 */
	public boolean isConnected()
	{
		return _status == ConnectionStatus.CONNECTED;
	}

	public ConnectionStatus getStatus()
	{
		return _status;
	}

	/**
	 * Called by TCPListener when a (not-null) message was discovered.
	 * 
	 * @param message
	 *            The encountered message.
	 */
	void receiveMessage(String message)
	{
		_messageReceiver.receiveMessage(message);
	}

	void onConnectionLost()
	{
		_status = ConnectionStatus.CONNECTION_LOST;
		_messageReceiver.notifyConnectionLost();
		disconnect();
	}

	/**
	 * Helpmethod to create a TCPlistener when connected.
	 */
	void startMessageListening()
	{
		if (_status == ConnectionStatus.CONNECTED)
		{
			_msgListen = new TCPlistener(_in, this);
		}
	}

	//TODO: JAVADOCS
	void startMessageSending()
	{
		if (_status == ConnectionStatus.CONNECTED)
		{
			_msgSend = new TCPsender(_out, this);
		}
	}

	/**
	 * Prepare to send a new message
	 * 
	 * @param message
	 *            The message to send.
	 * @throws TCPconnectionException
	 *             When not connected, i.e. it is impossible to send a message.
	 */
	public void send(String message) throws TCPconnectionException
	{
		if (_status == ConnectionStatus.CONNECTED)
		{
			_msgSend.send(message);
		}
		else
		{
			throw new TCPconnectionException("TCPconnection.send(): Tried to send data while not connected");
		}
	}

	/**
	 * Immediately sends a message. Used for disconnect purposes. When sending multiple messages, please use the regular send instead. This method skips the send buffer thread.
	 * 
	 * @param msg
	 *            The message to send
	 * @throws Exception
	 *             When not connected, i.e. it is impossible to send a message.
	 */
	public void sendFast(String msg) throws Exception
	{
		if (_status == ConnectionStatus.CONNECTED)
		{
			_out.println(msg);
		}
		else
		{
			throw new TCPconnectionException("TCPconnection.sendFast(): Tried to send data while not connected");
		}
	}

	public void connect(String IP) throws TCPconnectionException
	{
		String[] nums = IP.split("\\.");
		if (nums.length != 4) throw new TCPconnectionException("TCPconnection.connect(): Failed to connect: invalid IP address");

		byte[] ipaddr = new byte[4];
		for (int i = 0; i < 4; i++)
		{
			int val = Integer.valueOf(nums[i]);

			if (val < 0 || val > 255)
			{
				throw new TCPconnectionException("TCPconnection.connect(): Failed to connect: invalid IP address (byte " + (i + 1) + " out of range)");
			}
			else if (val > 127)
			{
				ipaddr[i] = (byte) (-256 + val);
			}
			else
			{
				ipaddr[i] = (byte) val;
			}
		}

		connect(ipaddr);
	}

	/**
	 * Attempt to establish a TCP connection with another or the same computer.
	 * 
	 * @param IP
	 *            The ip-adress used by the computer you want to use for the other end of the connection.
	 * @throws TCPconnectionException
	 *             An ignorable when the status is not idle. An unignorable when the attempt to connect has failed.
	 */
	public void connect(byte[] IP) throws TCPconnectionException
	{
		if (_status == ConnectionStatus.IDLE)
		{
			_status = ConnectionStatus.CONNECTING;
			try
			{
				_address = InetAddress.getByAddress(IP);
				_socket = new Socket(_address, _port);
				_out = new PrintWriter(_socket.getOutputStream(), true);
				_in = new BufferedReader(new InputStreamReader(_socket.getInputStream()));
				_status = ConnectionStatus.CONNECTED;
				startMessageSending();
				startMessageListening();
			}
			catch (Exception e)
			{
				_status = ConnectionStatus.IDLE;
				throw new TCPconnectionException("TCPconnection.connect(): Failed to connect to the specified host: " + e.toString(), false);
			}
		}
		else
		{
			throw new TCPconnectionException("TCPconnection.connect(): Failed to connect: the TCPconnection state was not IDLE");
		}
	}
}